import { createAsyncThunk, createSlice, isRejected } from '@reduxjs/toolkit';
import { RootState } from './store';
import { request } from '../util/Net';
import { encryptSha256 } from '../util/Crypto';

// 1. Requests and Responses, for communication with backend.

// 1.1 Requests.

/**
 * Request to log in to the system.
 * @path /service/auth/login 
 */
interface LoginRequest {
  /**
   * User name.
   */
  userName: string;
  /**
   * Password encrypted in SHA-256, 32 characters.
   */
  password: string;
};


/**
 * Request to register a new user.
 * @path /service/auth/register
 */
interface RegisterRequest {
  /**
   * User name, must be unique.
   */
  userName: string;
  /**
   * Password encrypted in SHA-256, 32 characters.
   */
  password: string;
  /**
   * Invite code with 6 characters, must be unique, determining the authority of the new user.
   * This field can be empty.
   */
  inviteCode: string;
}

// 1.2 Responses.

/**
 * Response from log in module.
 * @errcode 10001
 * @path /service/auth/login
 */
interface LoginResponse {
  /**
   * JWT Token.
   */
  token: string;
  /**
   * Authority of the user.
   */
  authority: string;
};

/**
 * Response from register module.
 * @errcode 10003
 * @path /service/auth/login
 */
interface RegisterResponse {
  /**
   * JWT Token.
   */
  token: string;
  /**
   * Authority of the user.
   */
  authority: string;
};

// 2. Params for reducers.

/**
 * Params for action login.
 */
interface LoginParams {
  /** 
   * User name. 
   */
  userName: string;
  /** 
   * Plain text password.
   */
  password: string;
};

/**
 * Params for register.
 */
interface RegisterParams {
  /**
   * User name, must be unique.
   */
  userName: string;
  /**
   * Plain text password.
   */
  password: string;
  /**
   * Invite code with 6 characters, must be unique, determining the authority of the new user.
   * This field can be empty.
   */
  inviteCode: string;
}

// 3. State and initial value definition.

/**
 * Redux state repository for this slice.
 */
interface AuthState {
  /**
   * Instruct if the current user is logged in.
   */
  loggedIn: boolean;
  /**
   * Saved token. Load token from local storage if possible.
   */
  token: string;
  /**
   * Instruct if the token is loaded.
   */
  tokenLoaded: boolean;
  /**
   * Authority of the user.
   */
  authority: string;
  /**
   * Instruct if password is wrong.
   */
  passwordError: boolean;
  /**
   * Instruct if user does not exist.
   */
  userError: boolean;
  /**
   * Instruct if invite code is not valid.
   */
  inviteCodeError: boolean;
  /**
   * Error message of invite code.
   */
  inviteCodeErrorMessage: string;
};

/**
 * Initial state of this slice.
 */
const initialState: AuthState = {
  loggedIn: false,
  token: '',
  tokenLoaded: false,
  authority: '',
  passwordError: false,
  userError: false,
  inviteCodeError: false,
  inviteCodeErrorMessage: ''
};

// 4. Async functions definition.

/**
 * Async reducer.
 * Login into the system, and update the state.
 * @param params params for login, including user name and plain text password
 * @example const dispatch = useAppDispatch();
 * dispatch(login({ userName: 'user_name', password: 'plain_password' }));
 */
const login = createAsyncThunk('api/login', async (params: LoginParams) => {
  // Encrypt the plain text password.
  const passwordHash: string = encryptSha256(params.password);

  // Send request, without token.
  const response = await request<LoginResponse, LoginRequest>('/service/auth/login', {
    userName: params.userName,
    password: passwordHash
  }, false);

  // Return the json response of the net communication.
  return response.data;
});

/**
 * Async reducer.
 * Register a new user, and update the state.
 * @param params params for register, including user name, plain text password and invite code
 * @example const dispatch = useAppDispatch();
 * dispatch(register({ userName: 'user_name', password: 'plain_password', inviteCode: 'a1s2f6' }));
 */
const register = createAsyncThunk('api/register', async (params: RegisterParams) => {
  // Encrypt the plain text password.
  const passwordHash: string = encryptSha256(params.password);

  // Send request, without token.
  const response = await request<RegisterResponse, RegisterRequest>('/service/auth/register', {
    userName: params.userName,
    password: passwordHash,
    inviteCode: params.inviteCode
  }, false);

  // Return the json response of the net communication.
  return response.data;
});

// 5. Slice definition.

/**
 * Slice of the module Auth.
 */
const authSlice = createSlice({
  name: 'auth',
  initialState: initialState,
  reducers: {
    /**
     * Clean all auth information in this slice.
     * @example import { authActions } from './Auth.ts';
     * const dispatch = useAppDispatch();
     * dispatch(authActions.clean());
     */
    clean: (state) => {
      state.loggedIn = false;
      state.token = '';
      state.authority = '';
      localStorage.removeItem('token');
    },
    loadToken: (state) => {
      const token: string | null = localStorage.getItem('token');
      state.tokenLoaded = true;
      if (token !== null) {
        state.token = token;
        state.loggedIn = true;
      }
    },
    clearError: (state) => {
      state.userError = false;
      state.passwordError = false;
      state.inviteCodeError = false;
      state.inviteCodeErrorMessage = '';
    }
  },
  extraReducers: (builder) => {
    builder.addCase(login.fulfilled, (state, action) => {
      if (action.payload.code === 0) {
        state.loggedIn = true;
        state.token = action.payload.result.token;
        state.authority = action.payload.result.authority;
        localStorage.setItem('token', action.payload.result.token);
      }
      else if (action.payload.code === 10001) {
        state.userError = true;
      }
      else if (action.payload.code === 10002) {
        state.passwordError = true;
      }
    }).addCase(register.fulfilled, (state, action) => {
      switch(action.payload.code) {
        case 0:
          state.loggedIn = true;
          state.token = action.payload.result.token;
          state.authority = action.payload.result.authority;
          localStorage.setItem('token', action.payload.result.token);
          break;
        case 10201:
          state.userError = true;
          break;
        case 10202: case 10203: case 10204:
          state.inviteCodeError = true;
          state.inviteCodeErrorMessage = action.payload.message;
          break;
      }
    }).addMatcher(isRejected, (state, action) => {
      
    });
  }
});

// 6. Selectors for state in this module.

export const selectLoggedIn = (state: RootState) => state.Auth.loggedIn;
export const selectToken = (state: RootState) => state.Auth.token;
export const selectTokenLoaded = (state: RootState) => state.Auth.tokenLoaded;
export const selectUserError = (state: RootState) => state.Auth.userError;
export const selectPasswordError = (state: RootState) => state.Auth.passwordError;
export const selectInviteCodeError = (state: RootState) => state.Auth.inviteCodeError;
export const selectInviteCodeErrorMessage = (state: RootState) => state.Auth.inviteCodeErrorMessage;

// 7. Export necessary modules.

export { login, register };
export const authActions = authSlice.actions;
export default authSlice.reducer;
